/* jshint esversion: 9 */
import { getGaussiumWeightMatrix, textureBlur, put, whiteTransparent } from "./gl.js"
import { getRenderedSize, Text } from "./text.js"
import * as twgl from './twgl-full.module.js'

export const lyric_line_blur_radius = 20
class LyricLine {
    constructor(time, text, y, fontsize, exittime, curves, player) {
        this.gl = player.gl
        this.canvas = this.gl.canvas
        this.time = time
        this.text = text
        this.fontsize = fontsize
        const size = getRenderedSize(text, fontsize, this.canvas.width * 0.7)
        this.width = size[0]; this.height = size[1]
        // delete this.renderedTextImageData //We only extract the width and height
        this.exittime = exittime
        this.y = y
        /*For y, we take downwards as positive and upwards as negative.*/
        this.curves = curves
        this.player = player

        this.animations = []
        this.renderPrepared = false
    }
    initWebglResources() {
        // Some code in this block requires this.player.blurTmpFb, which can't be created until we know the max width/height of all the lyric lines, which is calculated in the initialization process of each lyric line. So we can't do this at initialization.
        if (this.renderPrepared == false) {
            console.log("Loading", this.text)
            this.renderedTextImageData = Text(this.text, this.fontsize, this.canvas.width * 0.7)
            const texture = twgl.createTexture(this.gl, { src: this.renderedTextImageData })
            delete this.renderedTextImageData
            this.blackTextFb = twgl.createFramebufferInfo(this.gl, [
                {
                    attachment: texture
                }
            ], this.width, this.height)
            twgl.bindFramebufferInfo(this.gl, null) //twgl.js leaves it bound, and upcoming operations may affect this framebuffer. So unbind it.
            this.textFb = twgl.createFramebufferInfo(this.gl, undefined, this.width, this.height)
            whiteTransparent(this.gl, this.blackTextFb, this.textFb)
            this.gl.deleteTexture(this.blackTextFb.attachments[0])
            this.gl.deleteFramebuffer(this.blackTextFb.framebuffer)
            const xr = this.width / this.player.blurTmpFb.width, yr = this.height / this.player.blurTmpFb.height //x ratio, y ratio
            const textureCoordinateData = [
                0, 1,
                0, 1 - yr,
                xr, 1,
                xr, 1 - yr
            ]
            this.webglResources = {
                blur: {
                    y_textureCoordinateData: textureCoordinateData,
                    bufferInfos: {
                        x: twgl.createBufferInfoFromArrays(this.gl, {
                            a_position: {
                                numComponents: 2,
                                data: [
                                    -1, 1,
                                    -1, 1 - 2 * yr,
                                    -1 + 2 * xr, 1,
                                    -1 + 2 * xr, 1 - 2 * yr
                                ]
                            },
                            a_textureCoordinate: {
                                numComponents: 2,
                                data: [
                                    0, 1,
                                    0, 0,
                                    1, 1,
                                    1, 0
                                ],
                            }
                        }),
                        y: twgl.createBufferInfoFromArrays(this.gl, {
                            a_position: {
                                numComponents: 2,
                                data: [
                                    -1, 1,
                                    -1, 1 - 2 * yr,
                                    -1 + 2 * xr, 1,
                                    -1 + 2 * xr, 1 - 2 * yr
                                ]
                            },
                            a_textureCoordinate: {
                                numComponents: 2,
                                data: textureCoordinateData
                            }
                        })
                    }
                }
            }
        }

        this.renderPrepared = true
    }
    releaseWebglResources() {
        if (this.renderPrepared == true) {
            const gl = this.gl
            const xbip = this.webglResources.blur.bufferInfos.x.attribs.a_position.buffer,
                xbit = this.webglResources.blur.bufferInfos.x.attribs.a_textureCoordinate.buffer,
                ybip = this.webglResources.blur.bufferInfos.y.attribs.a_position.buffer,
                ybit = this.webglResources.blur.bufferInfos.y.attribs.a_textureCoordinate.buffer
            //x,y buffer into position,texture
            gl.deleteBuffer(xbip)
            gl.deleteBuffer(xbit)
            gl.deleteBuffer(ybip)
            gl.deleteBuffer(ybit)
            delete this.webglResources

            gl.deleteFramebuffer(this.textFb.framebuffer)
            gl.deleteTexture(this.textFb.attachments[0])
            delete this.textFb
        }

        this.renderPrepared = false
    }
    onScreen(extendHeight = 0) {
        //extendHeight: For preloading
        return this.y < this.canvas.height + extendHeight + 1 && this.y + this.height >= 1
    }
    onScreenIndex() {
        const distance = this.y - this.player.firstLineOffset,
            index = distance / (this.player.h + this.player.space)

        return index
    }
    move(t, dt) {
        this.animations.forEach(animation => {
            animation(t, dt)
        });

    }
    render() {
        if (this.onScreen() && this.renderPrepared) {
            //Check this.renderPrepared, because when the page is out of screen for a while, some lines are instantly released but the animation has not been played yet (They are still on screen)
            const r = Math.trunc(this.curves.r(Math.abs(this.y - this.player.firstLineOffset)))
            const resize_to = this.curves.size(Math.min(Math.abs(this.onScreenIndex()), 1)) //0~1
            var alpha = this.curves.alpha(Math.min(Math.abs(this.onScreenIndex()), 1))
            const left = this.gl.drawingBufferWidth / 2 - this.width * resize_to / 2,
                right = this.gl.drawingBufferWidth / 2 + this.width * resize_to / 2,
                h = this.gl.canvas.height
            const targetPositions = [
                left, h - (this.y + this.height * resize_to),
                left, h - this.y,
                right, h - (this.y + this.height * resize_to),
                right, h - this.y,
            ]
            const srcPos = this.webglResources.blur.y_textureCoordinateData
            if (r > 0) {
                const uniform = {
                    u_image: 0, //To be filled by textureBlur
                    u_textureSize: [0, 0], //To be filled by textureBlur
                    matrix: this.player.matrixTextures[r],
                    matrix_sum: this.player.matrixSums[r]
                }
                const uniforms = {
                    x: uniform,
                    y: uniform
                }
                textureBlur(this.gl, this.textFb, this.player.blurTmpFb, this.player.blurredFb, this.webglResources.blur.bufferInfos, uniforms, r)
                put(this.gl, targetPositions, this.player.blurredFb, null, alpha, srcPos)
            } else {
                put(this.gl, targetPositions, this.textFb, null, alpha)
            }
        }
    }
}

const default_lyric_height = 80 * window.devicePixelRatio
export class LyricPlayer {
    constructor(canvas, lyrics_string, lyric_height = default_lyric_height, space = default_lyric_height * 0.3, first_line_offset = default_lyric_height * 2.6, u = 100, g = 10) {
        /*
        param space: The space between lines
        */
        this.canvas = canvas
        this.gl = this.canvas.getContext("webgl", { premultipliedAlpha: false })
        this.lyrics_string = lyrics_string
        this.willplay_index = 1
        this.u = u
        this.g = g
        this.lyricLines = []
        this.h = lyric_height
        this.space = space
        this.firstLineOffset = first_line_offset
        this.playing = false

        const curves = {
            size: resizeCurve.getCurve(1, 1),
            r: blurRadiusCurve.getCurve(this.canvas.height, lyric_line_blur_radius),
            alpha: alphaCurve.getCurve(1, 1)
        }

        const firstLine = new LyricLine(0, "· · ·", this.firstLineOffset, this.h, 0, curves, this)
        let y = firstLine.y + firstLine.height + this.space
        this.lyricLines.push(firstLine)
        let maxHeight = firstLine.height, maxWidth = firstLine.width
        const lines = lyrics_string.includes("\r\n") ? lyrics_string.split("\r\n") : lyrics_string.split("\n")
        for (let line_index = 0; line_index < lines.length; line_index++) {
            try {
                const lyric_line_raw = lines[line_index],
                    [time_raw, lyric_line_content] = lyric_line_raw.split(']'),
                    time_string = time_raw.substring(1),
                    [minute_string, second_string] = time_string.split(':'),
                    [minute, second] = [Number(minute_string), Number(second_string)],
                    time = minute * 60 + second
                for (let i of [lyric_line_raw, time_raw, lyric_line_content, minute_string, second_string, minute, second]) {
                    //(""==0)==true. Fuck javascript.
                    if ((typeof (i) == "string" && i == "") || (typeof (i) == "number" && i == NaN)) {
                        throw "Invalid lyric line"
                    }
                }
                const newLine = new LyricLine(time, lyric_line_content, y, this.h, time, curves, this)
                y += newLine.height + this.space
                this.lyricLines.push(newLine)
                maxHeight = Math.max(newLine.height, maxHeight); maxWidth = Math.max(newLine.width, maxWidth)
            } catch (err) {
                //lrc file format issues, ignore them
            }
        }
        this.blurTmpFb = twgl.createFramebufferInfo(this.gl, [{ format: this.gl.RGBA }], maxWidth, maxHeight)
        this.gl.clearColor(1, 1, 1, 0)
        this.gl.clear(this.gl.COLOR_BUFFER_BIT)
        this.blurredFb = twgl.createFramebufferInfo(this.gl, [{ format: this.gl.RGBA }], maxWidth, maxHeight)
        this.gl.clearColor(1, 1, 1, 0)
        this.gl.clear(this.gl.COLOR_BUFFER_BIT)

        this.matrixTextures = []
        this.matrixSums = []
        for (let r = 1; r <= lyric_line_blur_radius; r++) {
            const matrix = getGaussiumWeightMatrix(r)
            this.matrixTextures[r] = twgl.createTexture(this.gl, {
                format: this.gl.LUMINANCE,
                src: matrix.matrix,
                width: 2 * r + 1
            })
            this.matrixSums[r] = matrix.sum
        }
        this.prepareWebglResources()
        //TODO: support exittime
    }

    prepareWebglResources() {
        console.log("Preloading using the height of", this.lyricLines[this.willplay_index - 1].text)
        const extendHeight = this.lyricLines[this.willplay_index - 1].height + this.space
        for (let i = this.willplay_index - 1; i < this.lyricLines.length; i++) {
            const line = this.lyricLines[i]
            if (line.onScreen(extendHeight)) {
                line.initWebglResources()
            } else {
                break
                //We are going from up to down
            }
        }
    }

    move(t, dt) {
        //t: time since started playing, in seconds
        // duration: How long will one line's animation last
        const duration = 0.6
        if (this.willplay_index < this.lyricLines.length) {
            const l = this.lyricLines[this.willplay_index]
            if (t >= l.time - duration) {
                const targetMove = this.lyricLines[this.willplay_index - 1].height + this.space,
                    player = this, starttime = t
                let screenTopLineIndex
                for (let i = 0; i < this.lyricLines.length; i++) {
                    if (this.lyricLines[i].onScreen()) {
                        screenTopLineIndex = i
                        break;
                    }
                }
                this.prepareWebglResources()
                for (let i = screenTopLineIndex;
                    i < this.lyricLines.length;
                    i++) {
                    const relativeIndex = i - (player.willplay_index - 1)
                    const k = 2 * targetMove / (duration ** 2),
                        pos = positionCurve.getCurve(duration, targetMove)
                    let lastxt = 0
                    const a = function (t, dt) {
                        //dt: time since last frame ( or, last call of this function )
                        const xt = t - relativeIndex * 0.015 - starttime // Time since animation start
                        if (xt > 0 && xt <= duration) {
                            const x = pos(xt) - pos(lastxt)
                            player.lyricLines[i].y -= x
                            lastxt = xt
                        } else if (xt > duration) {
                            //Animation completed
                            const x = pos(xt) - pos(lastxt)
                            player.lyricLines[i].y -= x
                            const line = player.lyricLines[i]
                            for (let o = 0; o < player.lyricLines[i].animations.length; o++) {
                                if (player.lyricLines[i].animations[o] == a) {
                                    delete line.animations[o]
                                    break
                                }
                            }
                            if (line.onScreenIndex() < 0 && line.onScreen() == false) {
                                line.releaseWebglResources()
                            }
                        }
                        //else if xt<=0: stay still
                    }
                    player.lyricLines[i].animations.push(a)
                }
                this.willplay_index++;
            }
        }

        for (let i of this.lyricLines) i.move(t, dt)
    }

    render() {
        twgl.bindFramebufferInfo(this.gl, null)
        this.gl.clearColor(1, 1, 1, 0)
        this.gl.clear(this.gl.COLOR_BUFFER_BIT)
        for (let i of this.lyricLines) i.render()
    }

    play(time = 0) {
        /*
        param time: start playing from where?
        in seconds.

        Internally, we use ms.
        When passing things to move(), we use seconds, which can have decimal.
        */
        this.starttime = (new Date()).getTime() - time * 1000
        this.lasttime = 0
        let player = this
        let move_wrapper = function (timestamp) {
            const ms = (new Date()).getTime() - player.starttime,
                t = ms / 1000,
                dt = t - player.lasttime
            player.lasttime = t

            player.move(t, dt)
            player.render()

            if (player.playing) window.requestAnimationFrame(move_wrapper)
        }
        this.playing = true
        window.requestAnimationFrame(move_wrapper)
    }

}



class Curve {
    constructor(prototypeFunction) {
        /*
        A Curve consists of prototypeFunction, definition domain and value domain.
        The definition domain and value domain of prototypeFunction must be [0,1].
        Then we'll scale according to, say, screensize.
        */
        this.prototypeFunction = prototypeFunction
    }
    getCurve(x, y) {
        /*
        param x: target definition domain
        param y: target value domain
        */
        let pr = this.prototypeFunction
        let curve = function (input) {
            let xx = input / x
            let res = pr(xx)
            let ret = res * y
            return ret
        }
        return curve
    }
}

//Draw these curves on geogebra.org, and you'll know what are they.
let oldresizeCurve = new Curve(function (x) {
    const a = x ** 2,
        b = -100 * a,
        c = 2 ** b,
        d = 0.2 * c,
        e = d + 0.8,
        f = Math.max(e, 0.01)
    return f
})
let resizeCurve = new Curve(function (x) {
    const f = function (x) {
        const a = x - 3 ** 0.5 / 2,
            b = 1 - a ** 2,
            c = b ** 0.5 - 1 / 2
        return c
    },
        g = function (x) {
            return f(2 / 3 ** 0.5 * (x - 0.5)) + 0.5
        },
        h = function (x) {
            if (x >= 0.5) {
                return g(x)
            } else {
                return 1 - g(1 - x)
            }
        }
    return 0.9 + h(1 - x) * 0.1

})
let oldalphaCurve = new Curve(function (x) {
    const a = x ** 2,
        b = -50 * a,
        c = 2 ** b,
        d = 0.5 * c,
        e = d + 0.2,
        // 0.2<=alpha<=0.7
        f = Math.max(e, 0.01)
    return f
})
let alphaCurve = new Curve(function (x) {
    const f = function (x) {
        const a = x - 3 ** 0.5 / 2,
            b = 1 - a ** 2,
            c = b ** 0.5 - 1 / 2
        return c
    },
        g = function (x) {
            return f(2 / 3 ** 0.5 * (x - 0.5)) + 0.5
        },
        h = function (x) {
            if (x >= 0.5) {
                return g(x)
            } else {
                return 1 - g(1 - x)
            }
        }
    return 0.3 + h(1 - x) * 0.4

})
let blurRadiusCurve = new Curve(function (x) {
    const k = 1 / Math.log2(11)
    return k * Math.log2(10 * x + 1)
})
let positionCurve = new Curve(function (x) {
    if (x <= 0) return 0
    else if (x <= 1) return ((x - 1) ** 5) + 1
    else return 1
})

